package parsing.productions;

import java.util.List;
import java.util.LinkedList;
import java.util.Random;

import parsing.InvalidSyntaxException;
import world.Critter;
import world.CritterWorld;
import world.Hex;
import world.Location;


public class Sensor extends MutableNode implements NumberNode {
	// Invariant for children: if type is FOOD or DAMAGE, children is empty.
	// If type is NEARBY or AHEAD, children has one Term.
	
	public static final int NEARBY	= 1;
	public static final int AHEAD 	= 2;
	public static final int DAMAGE	= 3;
	public static final int FOOD 	= 4;
	
	private int type; // Type of sensor. Only has one type.
	
	/**
	 * Constructs a new Sensor
	 * @param st Type of Sensor to construct
	 * @param t Term that the Sensor operates on
	 * Checks: if t is NEARBY or AHEAD, this Sensor has a Term.
	 */
	public Sensor(int st, Term t){
		super();
		if ((st == NEARBY) || (st == AHEAD)) {
			if (t == null) {
				throw new InvalidSyntaxException("NEARBY and AHEAD types must have a Term");
			}
			children.add(t);
		}
		type = st;
	}
	
	/**
	 * Constructs a new Sensor of type FOOD or DAMAGE
	 * Checks: st is not NEARBY or AHEAD
	 * @param st Type of Sensor to construct
	 * @throws IllegalArgumentException if st is not FOOD or DAMAGE
	 */
	public Sensor(int st){
		this(st, null);
	}

	@Override
	public void prettyPrint(StringBuffer sb) {
		switch(type){
		case DAMAGE: sb.append("damage "); break;
		case FOOD: sb.append("food "); break;
		case NEARBY:
			sb.append("nearby[");
			children.get(0).prettyPrint(sb);
			sb.deleteCharAt(sb.length() - 1);//removes the space before the ]
			sb.append("] ");
			break;
		case AHEAD:
			sb.append("ahead[");
			children.get(0).prettyPrint(sb);
			sb.deleteCharAt(sb.length() - 1);//removes the space before the ]
			sb.append("] ");
		}
	}

	@Override
	public void mutate(Program genome) {
//		StringBuffer sb = new StringBuffer();
//		prettyPrint(sb);
//		sb.append("was changed to ");
		Random rnd = new Random();
		if (type == FOOD || type == DAMAGE) { // Mutates by switching types.
			int newType;
			do {
				newType = rnd.nextInt(4) + 1;
			} while (type == newType);
			type = newType;
			if (type == AHEAD || type == NEARBY) {
				children.add(AbstractNode.generateTerm(genome, null));
			}
		} else { // type is AHEAD or NEARBY
			int mutationType = rnd.nextInt(3);
			if (mutationType == 0) { // Mutates by switching types.
				int newType;
				do {
					newType = rnd.nextInt(4) + 1;
				} while (type == newType);
				type = newType;
				if (type == FOOD || type == DAMAGE) children.clear();
			} else { // Replace Term with randomly generated Term.
				Term newTerm = AbstractNode.generateTerm(genome, children.get(0));
				children.set(0, newTerm);
			}
		}
//		prettyPrint(sb);
//		System.out.println(sb);
	}

	@Override
	public Node duplicate() {
		if (type == FOOD || type == DAMAGE) {
			return new Sensor(type);
		} else { // type is AHEAD or NEARBY. children has one Term.
			Term child= (Term)children.get(0).duplicate();
			return new Sensor(type, child);
		}
	}

	/**
	 * Checks if this Sensor is the same as n
	 * @param n the node to check against
	 * @return true if n is a Sensor and its type is the same as this one's
	 */
	@Override
	public boolean equals(Node n){
		if(!(n instanceof Sensor))
			return false;
		Sensor sNode = (Sensor) n;
		if(type != sNode.type)
			return false;
		if(type == AHEAD || type == DAMAGE)//if this Sensor has children and everything else lines up, check the children
			return super.equals(sNode);
		return true;//type is the same and no children, so true
	}

	@Override
	public double getValue(Critter c) {
		CritterWorld world;
		Location loc;
		switch(type){
		case NEARBY:
			world = c.getWorld();
			loc = c.getHex().getLocation();
			int dir = (int) ((Term) children.get(0)).getValue(c);
			dir = (Math.abs(dir) + c.getDirection()) % 6;
			Hex nearHex = world.getHex(loc.nextLocation(dir));
			if(nearHex == null || nearHex.isRock())
				return world.ROCK_VALUE;
			if(nearHex.hasCritter())
				return nearHex.getCritter().getMemValue(5);
			else//empty
				return -nearHex.getEnergy();
		case AHEAD:
			world = c.getWorld();
			loc = c.getHex().getLocation();
			int direction = c.getDirection();
			int dist = (int) ((Term) children.get(0)).getValue(c);
			if(dist < 0) dist = -dist;
			Location aheadLoc = loc;
			for(int i = 0; i < dist; i++)
				aheadLoc = aheadLoc.nextLocation(direction);
			Hex aheadHex = world.getHex(aheadLoc);
			if(aheadHex == null || aheadHex.isRock())
				return world.ROCK_VALUE;
			if(aheadHex.hasCritter()){
				if(aheadLoc.equals(loc)) return -aheadHex.getEnergy();
				else return aheadHex.getCritter().getMemValue(5);
			}
			else//empty
				return -aheadHex.getEnergy();
		case DAMAGE:
			return c.getLastDamage();
		default://FOOD
			return getSmartFoodValue(c);
		}
	}
	
	private int getSmartFoodValue(Critter c) {
		int rtn;
		CritterWorld world = c.getWorld();
		LinkedList<Hex> grayNodes = new LinkedList<Hex>();
		LinkedList<Hex> visitedHexes = new LinkedList<Hex>();
		Hex source = c.getHex();
		if(source.getEnergy() > 0)
			return 1;
		source.distance = 0;
		grayNodes.add(source);
		visitedHexes.addFirst(source);
		Hex closestFood = null;
		while (!grayNodes.isEmpty()) {
			Hex curNode = grayNodes.remove();
			if (curNode.getEnergy() > 0) {
				closestFood = curNode;
				break;
			}
			List<Hex> adjacent = curNode.getReachableHexes();
			if (curNode.distance < world.MAX_SMELL_DISTANCE) {
				for (Hex h : adjacent) {
					if (h.distance == -1) { // h is unvisited
						grayNodes.add(h);
						h.distance = curNode.distance + 1;
						visitedHexes.addFirst(h);
					}
				}
			}
		}
		if (closestFood == null) rtn = 0;
		else {
			Hex curPathHex = closestFood;
			int distFromCrit = curPathHex.distance;
			while (curPathHex.distance > 1) {
				List<Hex> adjacent = curPathHex.getReachableHexes();
				for (Hex h : adjacent) {
					if (h.distance == curPathHex.distance - 1) {
						curPathHex = h;
						break;
					}
				}
			}
			int absoluteDirection = -1;
			for (int i = 0; i < 6; i++) {
				if (world.getHex(source.getLocation().nextLocation(i)) == curPathHex)
					absoluteDirection = i;
			}
			if (absoluteDirection == -1)
				throw new RuntimeException("Critter food sensing failed. Path unable to be reconstructed.");
			int dirDifference = absoluteDirection - c.getDirection();//may be negative
			int dirRelativeToCritter = (dirDifference+6) % 6;//always between 0 and 5
			rtn = 6*distFromCrit + dirRelativeToCritter;
		}
		for (Hex h : visitedHexes) {
			h.distance = -1;
		}
		return rtn;
	}

	private int getFoodValue(Critter c){
		CritterWorld world = c.getWorld();
		Location loc = c.getHex().getLocation();
		Hex hex = world.getHex(loc);
		if(hex.getEnergy() > 0)
			return 1;
		int dirInCircle;
		loc = loc.nextLocation(0);
		for(int hexesAway = 1; hexesAway <= world.MAX_SMELL_DISTANCE; hexesAway++){
			int hexesInCircle = hexesAway * 6;
			dirInCircle = 2; // Start at the top of the circle and move southeast.
			int hexesInCircleChecked = 0;
			while (hexesInCircleChecked < hexesInCircle){
				hex = world.getHex(loc);
				if (hex != null && hex.getEnergy() > 0){
					int hexOnSide = hexesInCircleChecked % hexesAway;
					double positionOnSide = hexOnSide / (double) hexesAway;
					int dirFromCritter;
					if(positionOnSide <= .5)
						dirFromCritter = (dirInCircle + 4) % 6;
					else
						dirFromCritter = (dirInCircle + 5) % 6;
					int dirDifference = dirFromCritter - c.getDirection();//may be negative
					int dirRelativeToCritter = (dirDifference+6) % 6;//always between 0 and 5
					return 6 * hexesAway + dirRelativeToCritter;
				}
				hexesInCircleChecked++;
				loc = loc.nextLocation(dirInCircle);
				if(hexesInCircleChecked % hexesAway == 0)
					dirInCircle = (dirInCircle+1)%6;
			}
			loc = loc.nextLocation(0); // Move to the top of the next circle.
		}
		//no food or plants found
		return 0;
	}
}
